/*
 ------------------------------------------------------------------------------
 Copyright (C) 2013 Eternal Games.

 This file is part of the EternalTech source code.

 The EternalTech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The EternalTech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the EternalTech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// cl_main.cpp - main client file
//


#include "client.h"


clientActive_t				cl;
clientConnection_t			clc;
clientStatic_t				cls;

cvar_t *					cl_timeNudge;
cvar_t *					cl_deltaCompress;
cvar_t *					cl_showTimeDelta;
cvar_t *					cl_showMessages;
cvar_t *					cl_showRate;
cvar_t *					cl_drawFPS;
cvar_t *					cl_voiceChat;
cvar_t *					cl_voiceChatMode;
cvar_t *					cl_maxPackets;
cvar_t *					cl_dupPackets;
cvar_t *					cl_nextCinematic;
cvar_t *					cl_timeOut;


/*
 ==================
 CL_CanLaunchEditor
 ==================
*/
bool CL_CanLaunchEditor (const char *editor){

	if (cls.state != CS_ACTIVE || clc.demoPlaying){
		Com_Printf("You must be in a level to launch the %s editor\n", editor);
		return false;
	}

	if (!CVar_AllowCheats()){
		Com_Printf("You must enable cheats to launch the %s editor\n", editor);
		return false;
	}

	if (Sys_IsWindowFullscreen()){
		Com_Printf("You must be running the game in windowed mode to launch the %s editor\n", editor);
		return false;
	}

	if (!NET_IsLocalAddress(clc.netChan.remoteAddress)){
		Com_Printf("You must be running a local server to launch the %s editor\n", editor);
		return false;
	}

	if (cl.serverPure){
		Com_Printf("You must be running an unpure server to launch the %s editor\n", editor);
		return false;
	}

	return true;
}

/*
 ==================
 CL_LoadAssets
 ==================
*/
static void CL_LoadAssets (){

	// Load the materials
	cls.whiteMaterial = R_RegisterMaterialNoMip("white");
	cls.consoleMaterial = R_RegisterMaterialNoMip("console");
	cls.consoleInputMaterial = R_RegisterMaterialNoMip("consoleInput");
	cls.charsetMaterial = R_RegisterMaterialNoMip("charset");

	// Load the main menu GUI
	cls.mainMenuGUI = GUI_Register("mainMenu");
}

/*
 ==================
 CL_FullRestart

 Called by CL_RestartVideo_f and CL_RestartSound_f to perform a full restart if
 needed (to reload the whole level)
 ==================
*/
static void CL_FullRestart (){

	// Free the level
	CG_FreeLevel();

	// No longer fully initialized
	cls.fullyInitialized = false;

	// Shutdown GUI system
	GUI_Shutdown();

	// Shutdown sound system and renderer
	S_Shutdown(true);
	R_Shutdown(true);

	// Initialize GUI system
	GUI_Init();

	// Load local assets
	CL_LoadAssets();

	// Fully initialized
	cls.fullyInitialized = true;

	// Load the level
	CG_LoadLevel();
}

/*
 ==================
 CL_InitAll

 Called before loading a level or after disconnecting from the server to
 initialize all the necessary subsystems
 ==================
*/
void CL_InitAll (){

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	// Initialize renderer and sound system
	R_Init(false);
	S_Init(false);

	// Initialize GUI system
	GUI_Init();

	// Load local assets
	CL_LoadAssets();

	// Fully initialized
	cls.fullyInitialized = true;
}

/*
 ==================
 CL_ShutdownAll

 Called before loading a level or after disconnecting from the server to shut
 down all the necessary subsystems
 ==================
*/
void CL_ShutdownAll (){

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	// No longer fully initialized
	cls.fullyInitialized = false;

	// Shutdown GUI system
	GUI_Shutdown();

	// Shutdown sound system and renderer
	S_Shutdown(false);
	R_Shutdown(false);
}

/*
 ==================
 CL_NextCinematic

 Called when a cinematic finishes.
 If the cl_nextCinematic variable is set, that cinematic will be played.
 ==================
*/
void CL_NextCinematic (){

	char	value[MAX_STRING_LENGTH];
	char	name[MAX_PATH_LENGTH];
	char	*ch;

	if (!cls.playingCinematic)
		return;

	// Stop the cinematic
	CIN_StopCinematic(cls.cinematicHandle);
	cls.cinematicHandle = 0;

	// Get the next cinematic and clear or modify the variable
	if (!cl_nextCinematic->value[0])
		return;

	Str_Copy(value, cl_nextCinematic->value, sizeof(value));

	ch = Str_FindChar(value, ';');
	if (!ch)
		CVar_SetString(cl_nextCinematic, "");
	else {
		*ch++ = 0;

		CVar_SetString(cl_nextCinematic, ch);
	}

	Str_Copy(name, value, sizeof(name));
	Str_DefaultFilePath(name, sizeof(name), "videos");
	Str_DefaultFileExtension(name, sizeof(name), ".RoQ");

	// Play the cinematic
	cls.cinematicHandle = CIN_PlayCinematic(name, CIN_SYSTEM);
}

/*
 ==================
 CL_CheckTimeOut
 ==================
*/
static void CL_CheckTimeOut (){

	if (cls.state >= CS_CONNECTED && cls.frameTime - clc.lastReceivedTime > SEC2MS(cl_timeOut->floatValue)){
		if (++clc.timeOutCount == 5)
			Com_Error(false, "Server connection timed out");

		return;
	}

	clc.timeOutCount = 0;
}

/*
 ==================
 CL_CheckForResend
 ==================
*/
static void CL_CheckForResend (){

	if (cls.state != CS_CONNECTING && cls.state != CS_CHALLENGING)
		return;

	// Don't spam the server with connection requests if already rejected
	if (clc.connectRejected)
		return;

	// Resend if we haven't gotten a reply yet
	if (cls.frameTime - clc.connectTime < RESEND_TIMEOUT)
		return;

	clc.connectTime = cls.frameTime;
	clc.connectCount++;

	switch (cls.state){
	case CS_CONNECTING:
		// Request a challenge
		NET_OutOfBandPrintf(NS_CLIENT, clc.serverAddress, "getChallenge");

		break;
	case CS_CHALLENGING:
		// Request a connection
		CVar_ClearModifiedFlags(CVAR_USERINFO);

		NET_OutOfBandPrintf(NS_CLIENT, clc.serverAddress, "connect %i %i %i \"%s\"", PROTOCOL_VERSION, clc.serverChallenge, CVar_GetVariableInteger("net_channelPort"), CVar_InfoString(CVAR_USERINFO));

		break;
	}
}

/*
 ==================
 CL_PrintStats
 ==================
*/
static void CL_PrintStats (){

	if (cl_showRate->integerValue){
		if (cls.state >= CS_CONNECTED)
			Com_Printf("incoming rate: %4i KB/sec, outgoing rate: %4i KB/sec\n", SIZE_KB(clc.netChan.incomingRateBytes), SIZE_KB(clc.netChan.outgoingRateBytes));
	}
}


/*
 ==============================================================================

 FRAMEWORK

 ==============================================================================
*/


/*
 ==================
 
 ==================
*/
void CL_Frame (int msec){

	int		timeClient;

	// If not running, dump any incoming packets
	if (cls.state == CS_UNINITIALIZED){
		CL_ReadPackets(true);
		return;
	}

	if (com_speeds->integerValue)
		timeClient = Sys_Milliseconds();

	cls.frameTime += msec;
	cls.frameMsec = msec;

	// Read and parse incoming packets
	CL_ReadPackets(false);

	// Check if the connection timed out
	CL_CheckTimeOut();

	// Resend a connection request if necessary
	CL_CheckForResend();

	// Let the mouse and controller devices generate events
	IN_PollDevices();

	// Process commands
	Cmd_ExecuteBuffer();

	// Send intentions now
	CL_SendCommands();

	// Run the game simulation
	CG_RunFrame();

	// Update the screen
	CL_UpdateScreen();

	// Update sounds
	S_Update(cls.frameTime);

	// Print statistics
	CL_PrintStats();

	if (com_speeds->integerValue)
		com_timeClient += (Sys_Milliseconds() - timeClient);
}


/*
 ==============================================================================

 CONSOLE COMMANDS

 ==============================================================================
*/


/*
 ==================
 CL_Connect_f
 ==================
*/
static void CL_Connect_f (){

	if (Cmd_Argc() != 2){
		Com_Printf("Usage: connect <server>\n");
		return;
	}

	Str_Copy(cls.serverName, Cmd_Argv(1), sizeof(cls.serverName));

	// If running a local server, kill it
	SV_Shutdown("Server quit");

	// If connected to a server, disconnect
	CL_Disconnect(true);

	// Resolve server address
	if (!NET_StringToAddress(cls.serverName, &clc.serverAddress)){
		Com_Printf("Couldn't resolve %s\n", cls.serverName);
		return;
	}

	if (clc.serverAddress.port == 0)
		clc.serverAddress.port = BigShort(PORT_SERVER);

	if (NET_IsLocalAddress(clc.serverAddress)){
		Com_Printf("Can't connect to localhost\n");
		return;
	}

	Com_Printf("%s resolved to %s\n", cls.serverName, NET_AddressToString(clc.serverAddress));

	// CL_CheckForResend will fire immediately
	cls.state = CS_CONNECTING;

	clc.connectTime = cls.frameTime - RESEND_TIMEOUT;
	clc.connectCount = 0;
	clc.connectRejected = false;

	Com_Printf("Connecting to %s...\n", cls.serverName);
}

/*
 ==================
 CL_Reconnect_f
 ==================
*/
static void CL_Reconnect_f (){

	if (!cls.serverName[0]){
		Com_Printf("No server to reconnect\n");
		return;
	}

	// If running a local server, kill it
	SV_Shutdown("Server quit");

	// If connected to a server, disconnect
	CL_Disconnect(true);

	// Resolve server address
	if (!NET_StringToAddress(cls.serverName, &clc.serverAddress)){
		Com_Printf("Couldn't resolve %s\n", cls.serverName);
		return;
	}

	if (clc.serverAddress.port == 0)
		clc.serverAddress.port = BigShort(PORT_SERVER);

	if (NET_IsLocalAddress(clc.serverAddress)){
		Com_Printf("Can't reconnect to localhost\n");
		return;
	}

	Com_Printf("%s resolved to %s\n", cls.serverName, NET_AddressToString(clc.serverAddress));

	// CL_CheckForResend will fire immediately
	cls.state = CS_CONNECTING;

	clc.connectTime = cls.frameTime - RESEND_TIMEOUT;
	clc.connectCount = 0;
	clc.connectRejected = false;

	Com_Printf("Reconnecting to %s...\n", cls.serverName);
}

/*
 ==================
 CL_Disconnect_f
 ==================
*/
static void CL_Disconnect_f (){

	if (cls.state == CS_DISCONNECTED){
		Com_Printf("Not connected to a server\n");
		return;
	}

	Com_Error(false, "Disconnected from server");
}

/*
 ==================
 CL_Ping_f
 ==================
*/
static void CL_Ping_f (){

	netAdr_t	adr;

	if (Cmd_Argc() != 2){
		Com_Printf("Usage: ping <server>\n");
		return;
	}

	// Resolve server address
	if (!NET_StringToAddress(Cmd_Argv(1), &adr)){
		Com_Printf("Couldn't resolve %s\n", Cmd_Argv(1));
		return;
	}

	if (adr.port == 0)
		adr.port = BigShort(PORT_SERVER);

	Com_Printf("%s resolved to %s\n", Cmd_Argv(1), NET_AddressToString(adr));

	// Send a packet to the specified address
	Com_Printf("Pinging %s...\n", Cmd_Argv(1));

	NET_OutOfBandPrintf(NS_CLIENT, adr, "getInfo");
}

/*
 ==================
 
 ==================
*/
static void CL_RCon_f (){

}

/*
 ==================
 CL_UserInfo_f
 ==================
*/
static void CL_UserInfo_f (){

	Com_Printf("User info settings:\n");
	Com_Printf("-------------------\n");

	Info_Print(CVar_InfoString(CVAR_USERINFO), Com_Printf);
}

/*
 ==================
 
 ==================
*/
static void CL_DumpServerInfo_f (){

}

/*
 ==================
 CL_RestartVideo_f
 ==================
*/
static void CL_RestartVideo_f (){

	// If needed, perform a full restart so the level is reloaded
	if (cls.state >= CS_PRIMED){
		CL_FullRestart();
		return;
	}

	// No longer fully initialized
	cls.fullyInitialized = false;

	// Shutdown GUI system
	GUI_Shutdown();

	// Restart the renderer
	R_Shutdown(true);
	R_Init(true);

	// Initialize GUI system
	GUI_Init();

	// Load local assets
	CL_LoadAssets();

	// Fully initialized
	cls.fullyInitialized = true;
}

/*
 ==================
 CL_RestartSound_f
 ==================
*/
static void CL_RestartSound_f (){

	// If needed, perform a full restart so the level is reloaded
	if (cls.state >= CS_PRIMED){
		CL_FullRestart();
		return;
	}

	// No longer fully initialized
	cls.fullyInitialized = false;

	// Shutdown GUI system
	GUI_Shutdown();

	// Restart the sound system
	S_Shutdown(true);
	S_Init(true);

	// Initialize GUI system
	GUI_Init();

	// Load local assets
	CL_LoadAssets();

	// Fully initialized
	cls.fullyInitialized = true;
}


/*
 ==============================================================================

 INITIALIZATION & SHUTDOWN

 ==============================================================================
*/


/*
 ==================
 
 ==================
*/
void CL_Init (){

	if (com_dedicated->integerValue)
		return;		// Nothing running on the client

	if (cls.state != CS_UNINITIALIZED)
		return;		// Already running

	Com_Printf("-------- Client Initialization --------\n");

	cls.state = CS_DISCONNECTED;

	// Register variables
	cl_timeNudge = CVar_Register("cl_timeNudge", "0", CVAR_INTEGER, CVAR_CHEAT, "Time in milliseconds to add more or less latency", -1000, 1000);	
	cl_deltaCompress = CVar_Register("cl_deltaCompress", "1", CVAR_BOOL, CVAR_CHEAT, "Ask for delta compressed messages", 0, 0);
	cl_showTimeDelta = CVar_Register("cl_showTimeDelta", "0", CVAR_BOOL, CVAR_CHEAT, "Show time delta", 0, 0);	
	cl_showMessages = CVar_Register("cl_showMessages", "0", CVAR_BOOL, CVAR_CHEAT, "Show server messages", 0, 0);
	cl_showRate = CVar_Register("cl_showRate", "0", CVAR_BOOL, CVAR_CHEAT, "Show network rate", 0, 0);	
	cl_drawFPS = CVar_Register("cl_drawFPS", "0", CVAR_BOOL, CVAR_ARCHIVE, "Draws frames rendered per second", 0, 0);	
	cl_voiceChat = CVar_Register("cl_voiceChat", "3", CVAR_INTEGER, CVAR_ARCHIVE | CVAR_USERINFO, "Enable voice chat (1 = receive, 2 = send, 3 = receive and send)", 0, 3);
	cl_voiceChatMode = CVar_Register("cl_voiceChatMode", "0", CVAR_INTEGER, CVAR_ARCHIVE, "Voice chat mode (0 = automatically detect speech, 1 = push-to-talk)", 0, 1);	
	cl_maxPackets = CVar_Register("cl_maxPackets", "30", CVAR_INTEGER, CVAR_ARCHIVE, "Maximum number of packets to send per second", 30, 125);
	cl_dupPackets = CVar_Register("cl_dupPackets", "1", CVAR_INTEGER, CVAR_ARCHIVE, "Number of packets to resend", 0, 5);	
	cl_nextCinematic = CVar_Register("cl_nextCinematic", "", CVAR_STRING, 0, "Cinematic to be played next (or list of cinematics separated by semicolons)", 0, 0);	
	cl_timeOut = CVar_Register("cl_timeOut", "180.0", CVAR_FLOAT, 0, "Connection time-out time in seconds", 1.0f, 300.0f);
	
	// Add commands
	Cmd_AddCommand("connect", CL_Connect_f, "Connects to a server", NULL);
	Cmd_AddCommand("reconnect", CL_Reconnect_f, "Reconnects to the last server", NULL);
	Cmd_AddCommand("disconnect", CL_Disconnect_f, "Disconnects from server", NULL);
	Cmd_AddCommand("ping", CL_Ping_f, "Pings a server", NULL);	
	Cmd_AddCommand("rcon", CL_RCon_f, "Issues a remote console command", NULL);
	Cmd_AddCommand("userInfo", CL_UserInfo_f, "Shows user info", NULL);
	Cmd_AddCommand("dumpServerInfo", CL_DumpServerInfo_f, "Shows the remote server info", NULL);	
	Cmd_AddCommand("restartVideo", CL_RestartVideo_f, "Restarts the video subsystem", NULL);
	Cmd_AddCommand("restartSound", CL_RestartSound_f, "Restarts the sound subsystem", NULL);
	Cmd_AddCommand("recordDemo", CL_RecordDemo_f, "Records a demo", NULL);
	Cmd_AddCommand("stopRecording", CL_StopRecording_f, "Stops demo recording", NULL);
	Cmd_AddCommand("playDemo", CL_PlayDemo_f, "Plays a demo", Cmd_ArgCompletion_DemoName);
	Cmd_AddCommand("timeDemo", CL_TimeDemo_f, "Times a demo", Cmd_ArgCompletion_DemoName);

	// Initialize console
	Con_Init();

	// Initialize cinematic module
	CIN_Init();

	// Initialize renderer and sound system
	R_Init(true);
	S_Init(true);

	// Initialize input system
	IN_Init();

	// Initialize GUI system
	GUI_Init();

	// Load local assets
	CL_LoadAssets();

	// Fully initialized
	cls.fullyInitialized = true;

	// Set the com_clientRunning variable
	CVar_SetBool(com_clientRunning, true);

	Com_Printf("---------------------------------------\n");
}

/*
 ==================
 
 ==================
*/
void CL_Shutdown (){

	if (cls.state == CS_UNINITIALIZED)
		return;		// Nothing running on the client

	Com_Printf("-------- Client Shutdown --------\n");

	// No longer fully initialized
	cls.fullyInitialized = false;

	// Remove commands
	Cmd_RemoveCommand("connect");
	Cmd_RemoveCommand("reconnect");
	Cmd_RemoveCommand("disconnect");
	Cmd_RemoveCommand("ping");
	Cmd_RemoveCommand("rcon");
	Cmd_RemoveCommand("userInfo");
	Cmd_RemoveCommand("dumpServerInfo");
	Cmd_RemoveCommand("restartVideo");
	Cmd_RemoveCommand("restartSound");
	Cmd_RemoveCommand("recordDemo");
	Cmd_RemoveCommand("stopRecording");
	Cmd_RemoveCommand("playDemo");
	Cmd_RemoveCommand("timeDemo");

	// If connected to a server, disconnect
	CL_Disconnect(false);

	// Shutdown GUI system
	GUI_Shutdown();

	// Shutdown input system
	IN_Shutdown();

	// Shutdown sound system and renderer
	S_Shutdown(true);
	R_Shutdown(true);

	// Shutdown cinematic module
	CIN_Shutdown();

	// Shutdown console
	Con_Shutdown();

	// Wipe the entire clientStatic_t structure
	Mem_Fill(&cls, 0, sizeof(clientStatic_t));

	// Reset the com_clientRunning variable
	CVar_SetBool(com_clientRunning, false);

	Com_Printf("---------------------------------\n");
}